今天我們來說明如何將gt表格呈現於Panel app中,內容參考自官方Solar Zenith Angles範例及Panel主要維護者之一的Marc Skov Madsen在Holoviz論壇中所給出的範例(註1)。
Panel是一個類似於Streamlit或Gradio的web app框架,因為其內部使用Param來作為互動操作的核心,學習起來雖然有一定的難度,但是個人覺得其程式碼相對容易維護。此外,Panel與gt的搭配也比一般框架來的簡單,所以想在今天的內容分享給大家。
今天的內容將部署於Py.Cafe,是一個使用Pyodide為技術的Python app部署平台,讓我們可以在瀏覽器中直接執行Python。由於Panel可以使用Pyodide執行,所以是Py.Cafe平台推薦的選擇之一。此外,因為Polars尚無法於Py.Cafe上安裝,所以我們會使用Pandas來整理Dataframe。
今天的內容將分為四大部份:
程式碼將存在main_static.py及main_dynamic檔中,其中動態表格成果可以於Py.Cafe中觀看。
此外需留意幾乎所有的Panel app都會需要加上一行pn.extension(),其功用為呼叫所需的JavaScript程式或套件。
原先範例中是使用Polars整理Dataframe:
sza_pivot = (
    pl.from_pandas(sza)
    .filter((pl.col("latitude") == "20") & (pl.col("tst") <= "1200"))
    .select(pl.col("*").exclude("latitude"))
    .drop_nulls()
    .pivot(values="sza", index="month", on="tst", sort_columns=True)
)
我們使用Pandas改寫如下:
import panel as pn
from great_tables import GT, html
from great_tables.data import sza
@pn.cache
def get_sza_pivot():
    return (
        sza.assign(tst_int=lambda df_: df_.tst.astype(int))
        .query("latitude == '20' and tst_int <= 1200")
        .drop(["latitude", "tst_int"], axis=1)
        .dropna()
        .pivot(values=["sza"], index=["month"], columns=["tst"])
        .droplevel(0, axis=1)
        .reindex(
            [
                "jan",
                "feb",
                "mar",
                "apr",
                "may",
                "jun",
                "jul",
                "aug",
                "sep",
                "oct",
                "nov",
                "dec",
            ],
            axis=0,
        )
        .reset_index(names="month")
    )
我們將整理DataFrame的邏輯寫入在get_sza_pivot(),並使用pn.cache()裝飾在該函數上。
眼尖的您可能發現,今天的例子不僅是快取一個返回DataFrame的函數,我們也將整理的步驟加入其中。原因是如果DataFrame的整理邏輯不會改變的話,一起擺進快取內也是個不錯的選擇。
DataFrame整理的步驟如下:
int而來(註2)。axis=1即是指定刪除欄位對象為列(axis=0則是指定刪除欄位對象為行)。DataFrame.pivot()多出來的階層,其中axis=1即是指定刪除階層對象為列(axis=0則是指定刪除階層對象為行)。get_table()函數建立get_table(),將範例中的製表程式碼存於其內,並使用get_sza_pivot()的回傳值作為被GT所包裹的DataFrame:
def get_table() -> GT:
    return (
        GT(get_sza_pivot(), rowname_col="month")
        .data_color(
            domain=[90, 0],
            palette=["rebeccapurple", "white", "orange"],
            na_color="white",
        )
        .tab_header(
            title="Solar Zenith Angles from 05:30 to 12:00",
            subtitle=html("Average monthly values at latitude of 20°N."),
        )
        .sub_missing(missing_text="")
    )
FastListTemplate呈現表格這裡我們使用了Panel的FastListTemplate,其內部已經預設好許多樣式,且可通過參數快速調整。而我們所要做的只是需要將get_table()的回傳值main_content,指定給main參數。最後呼叫FastListTemplate.servable()來告知Panel,這將是我們想要Panel渲染的最終樣式。
# main_static.py
main_content = get_table()
pn.template.FastListTemplate(
    site="Panel",
    title="Great Tables",
    main=[main_content],
    main_layout=None,
    accent="#70409f",
).servable()
現在我們可以於命令列中執行下列指令:
panel serve main_static.py
接著打開瀏覽器前往預設網址,如http://127.0.0.1:5006/main_static,就可以看見我們於Panel中建立的靜態表格:

接下來我們想要將靜態表格修改為動態表格,目標是希望透過新增兩個widget來選擇顏色,而表格的背景漸層顏色能做出相對應的變化。
get_table()函數修改get_table()函數,使其接受color1及color2兩個參數,並將這兩個參數置入GT.data_color()的palette參數中。
# main_dynamic.py
def get_table(color1: str, color2: str) -> GT:
    return (
        GT(get_sza_pivot(), rowname_col="month")
        .data_color(
            domain=[90, 0],
            palette=[color1, "white", color2],
            na_color="white",
        )
        .tab_header(
            title="Solar Zenith Angles from 05:30 to 12:00",
            subtitle=html("Average monthly values at latitude of 20°N."),
        )
        .sub_missing(missing_text="")
    )
使用pn.widgets.ColorPicker來做為兩種顏色的選擇器,並使用Pn.Row將兩個ColorPicker包裹起來,以及使用pn.layout.HSpacer來加上一些水平空白。
# main_dynamic.py
color1 = pn.widgets.ColorPicker(name="Color Picker1", value="#663399")
color2 = pn.widgets.ColorPicker(name="Color Picker2", value="#FFA500")
colors = pn.Row(
    pn.layout.HSpacer(),
    color1,
    pn.layout.HSpacer(),
    color2,
    pn.layout.HSpacer(),
)
Panel提供許多可用來排版的widget,其名字都非常直觀,如Pn.Row就是一個橫向的排版widget,而pn.layout.HSpacer就是施加水平空白的widget。
pn.bind()連接表格與顏色選擇器使用pn.bind()來將get_table()函數與所需傳入的顏色連結起來(註3)。
# main_dynamic.py
bind_table = pn.bind(get_table, color1, color2)
table = pn.panel(bind_table, align="center")
main_content = pn.Column(colors, table)
pn.bind()就像是Panel版本的functools.partial(),可以連結一個callable與需要變動的widget。當widget進行變動時,Panel將會負責傳遞其變動後的值給所綁定的callable。一旦綁定完成後,您可以將此綁定結果,也視為是一種widget。
接著使用pn.panel來包裹bind_table,並設定align參數為「"center"」。
再來使用pn.Column將colors與table依垂直方向排版,並命名為main_content。
FastListTemplate呈現表格最後一樣使用FastListTemplate,並指定main參數為main_content後,呼叫FastListTemplate.servable()來渲染成果。
pn.template.FastListTemplate(
    site="Panel",
    title="Great Tables",
    main=[main_content],
    main_layout=None,
    accent="#70409f",
).servable()
再次於命令列中執行下列指令:
panel serve main_dynamic.py
接著打開瀏覽器前往預設網址,如http://127.0.0.1:5006/main_dynamic,就可以看見我們於Panel中建立的動態表格:

註1:今日改寫內容已事先取得Marc同意。
註2:其實這裡如果不新增「"tst_int"」欄(int型別),而是針對「"tst"」欄(str型別),使用DataFrame.query("latitude == '20' and tst_int <= '1200'")也是可以。但我個人不是很喜歡這種依賴str型別來比較大小的寫法,比較喜歡先轉換為數字之後再來比較大小,邏輯會比較清楚。
註3:pn.bind()不是唯一的連結方式。如果您對其它方式有興趣,可以參考小弟於官方文件中改寫的範例,會說明如何使用更有效率的param.rx()來連結。
本日所有程式碼可參考gt-panel repo。